Skip to content

Conversation

DexAsHisH
Copy link

@DexAsHisH DexAsHisH commented Oct 4, 2025

Description

Fixes #1774

This PR addresses a type safety issue where fieldMeta is typed as Record<DeepKeys<TData>, AnyFieldMeta> but is initialized as an empty object, causing runtime crashes when accessing field metadata during the first render.

Changes

  • ✅ Updated FormState interface to include undefined in fieldMeta type
  • ✅ Updated internal code to safely access fieldMeta with optional chaining
  • ✅ Added unit tests to verify fieldMeta behaviour
  • ✅ Added integration tests for React adapter
  • ✅ Updated documentation to clarify when fieldMeta is available

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Breaking Changes

This is a breaking change for TypeScript users. Code that previously accessed fieldMeta without null checks will now show type errors, preventing runtime crashes.

Migration Example:

// Before
const isValid = form.state.fieldMeta.name.isValid

// After (use optional chaining)
const isValid = form.state.fieldMeta.name?.isValid

// Or (check for undefined)
const isValid = form.state.fieldMeta.name 
  ? form.state.fieldMeta.name.isValid 
  : true

…avior

fieldMeta is typed as Record<DeepKeys<TData>, AnyFieldMeta> but is
initialized as an empty object. This causes TypeScript to incorrectly
assume that accessing any valid field key will return a defined
AnyFieldMeta object, leading to runtime crashes when accessing field
metadata during the first render.

Updated the fieldMeta type to include undefined to accurately reflect
that field metadata is only available after a field has been mounted.

BREAKING CHANGE: fieldMeta values are now typed as potentially undefined.
Code that accesses fieldMeta without null checks will now show TypeScript
errors. Use optional chaining or explicit undefined checks.

Fixes TanStack#1774
Copy link

changeset-bot bot commented Oct 4, 2025

🦋 Changeset detected

Latest commit: 39f1bf1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@tanstack/form-core Patch
@tanstack/angular-form Patch
@tanstack/form-devtools Patch
@tanstack/lit-form Patch
@tanstack/react-form Patch
@tanstack/solid-form Patch
@tanstack/svelte-form Patch
@tanstack/vue-form Patch
@tanstack/react-form-devtools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

nx-cloud bot commented Oct 5, 2025

View your CI Pipeline Execution ↗ for commit 39f1bf1

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 48s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2025-10-07 14:06:19 UTC

Copy link

pkg-pr-new bot commented Oct 7, 2025

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@1787

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@1787

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@1787

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@1787

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@1787

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@1787

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@1787

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@1787

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@1787

commit: 39f1bf1

Copy link
Contributor

@LeCarbonator LeCarbonator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I like the unit tests to prevent regression. There's a few things to clean up, but as far as the type changes go for FormApi, it looks fine by me!

@@ -0,0 +1,27 @@
---
'@tanstack/form-core': major
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't consider type changes a breaking change, especially not ones like this that try to prevent existing runtime errors from being undetected.

See https://tanstack.com/form/latest/docs/typescript for more info.

Suggested change
'@tanstack/form-core': major
'@tanstack/form-core': patch

Comment on lines +5 to +26
Make fieldMeta values optional to reflect runtime behavior and prevent crashes

BREAKING CHANGE: `fieldMeta` values are now typed as `Record<DeepKeys<TData>, AnyFieldMeta | undefined>` instead of `Record<DeepKeys<TData>, AnyFieldMeta>`. This accurately reflects that field metadata is only available after a field has been mounted.

**Why:** Previously, TypeScript allowed unchecked access to `fieldMeta` properties, leading to runtime crashes when accessing metadata of unmounted fields during the first render.

**What changed:** The type now includes `undefined` in the union, forcing developers to handle the case where a field hasn't been mounted yet.

**How to migrate:**

```typescript
// Before (crashes at runtime)
const isValid = form.state.fieldMeta.name.isValid

// After - use optional chaining
const isValid = form.state.fieldMeta.name?.isValid

// Or explicit undefined check
const fieldMeta = form.state.fieldMeta.name
if (fieldMeta) {
const isValid = fieldMeta.isValid
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how GH actions will handle descriptions like this. I assume it'll be okay? Either way, as mentioned above, types aren't considered breaking changes.

Comment on lines 17 to 34
it('should not crash when accessing properties on undefined fieldMeta with optional chaining', () => {
const form = new FormApi({
defaultValues: {
name: '',
email: '',
},
})

expect(() => {
const isValid = form.state.fieldMeta.name?.isValid
const isTouched = form.state.fieldMeta.name?.isTouched
const errors = form.state.fieldMeta.name?.errors
return { isValid, isTouched, errors }
}).not.toThrow()

expect(form.state.fieldMeta.name?.isValid).toBeUndefined()
expect(form.state.fieldMeta.name?.isTouched).toBeUndefined()
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unit test is unrelated to this library. Optional chaining is already part of JavaScript spec. Please remove this redundant test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*.spec.ts files imply they're runtime tests, while type tests go into *.test-d.ts.

  • Please rename the file to fieldMeta.spec.ts
  • Change the describe string since the file tests for meta accessing and not type safety.

Comment on lines 126 to 145
it('should allow conditional access patterns', () => {
const form = new FormApi({
defaultValues: {
name: '',
},
})

const isValid1 = form.state.fieldMeta.name?.isValid ?? true
expect(isValid1).toBe(true)

const fieldMeta = form.state.fieldMeta.name
let isValid2 = true
if (fieldMeta) {
isValid2 = fieldMeta.isValid
}
expect(isValid2).toBe(true)

const errors = form.state.fieldMeta.name?.errors ?? []
expect(errors).toEqual([])
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unit test is unrelated to the library. Nullish coalescing is already part of JavaScript spec. Please remove it.

Comment on lines 227 to 269
it('should handle Object.values on fieldMeta safely', () => {
const form = new FormApi({
defaultValues: {
field1: '',
field2: '',
field3: '',
},
})

const field1 = new FieldApi({
form,
name: 'field1',
})

field1.mount()

const fieldMetaValues = Object.values(form.state.fieldMeta).filter(Boolean)
expect(fieldMetaValues.length).toBeGreaterThan(0)
expect(fieldMetaValues.every((meta) => meta !== undefined)).toBe(true)
})

it('should type-check correctly with TypeScript', () => {
const form = new FormApi({
defaultValues: {
name: '',
},
})

const meta = form.state.fieldMeta.name

if (meta) {
const isValid: boolean = meta.isValid
const errors: unknown[] = meta.errors
expect(isValid).toBeDefined()
expect(errors).toBeDefined()
}

const isValid = form.state.fieldMeta.name?.isValid
const errors = form.state.fieldMeta.name?.errors

expect(isValid === undefined || typeof isValid === 'boolean').toBe(true)
expect(errors === undefined || Array.isArray(errors)).toBe(true)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two tests are already covered by ones above, so they're not really needed.

Copy link
Author

@DexAsHisH DexAsHisH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've made the changes. ❤️

@DexAsHisH DexAsHisH requested a review from LeCarbonator October 7, 2025 13:30
Copy link

codecov bot commented Oct 7, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.44%. Comparing base (6892ed0) to head (39f1bf1).
⚠️ Report is 26 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1787      +/-   ##
==========================================
+ Coverage   90.35%   90.44%   +0.09%     
==========================================
  Files          38       38              
  Lines        1752     1790      +38     
  Branches      444      453       +9     
==========================================
+ Hits         1583     1619      +36     
- Misses        149      151       +2     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

formMeta is initially undefined, but its type is missing an undefined union
2 participants